/**
 * 
 */
package jframe.core.plugin.dispatch;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jframe.core.dispatch.DispatchTarget;
import jframe.core.msg.ConfigMsg;
import jframe.core.msg.Msg;
import jframe.core.msg.PoisonMsg;
import jframe.core.plugin.Plugin;
import jframe.core.plugin.PluginRef;
import jframe.core.plugin.annotation.Message;
import jframe.core.plugin.annotation.MsgInterest;
import jframe.core.plugin.annotation.MsgRecv;

/**
 * TODO types filter
 * 
 * @author dzh
 * @date Oct 9, 2013 4:04:58 PM
 * @since 1.0
 */
public class DispatchTargetHandler implements InvocationHandler {

    private static final Logger LOG = LoggerFactory.getLogger(DispatchTargetHandler.class);

    private static final String M_Receive = "receive";

    private static final String M_InterestMsg = "interestMsg";

    private PluginRef _ref;

    private BlockingQueue<Msg<?>> _cache;

    // message types
    private int[] _types;

    private Map<String, WeakReference<Method>> _mc = new WeakHashMap<String, WeakReference<Method>>(6);

    public DispatchTargetHandler(PluginRef ref) {
        if (ref == null) {
            throw new NullPointerException("PluginRef is null");
        }
        this._ref = ref;
        update(ref.getPlugin());
    }

    public void update(Plugin plugin) {
        if (plugin == null)
            return;
        _mc.clear();
        Message ma = plugin.getClass().getAnnotation(Message.class);
        _types = ma.msgTypes();
        if (_types != null)
            Arrays.sort(_types);
        int maxCache = ma.msgMaxCache();
        if (_cache == null) {
            _cache = new LinkedBlockingDeque<Msg<?>>(maxCache);
        } else if (maxCache != (_cache.size() + _cache.remainingCapacity())) {
            // TODO miss message
            BlockingQueue<Msg<?>> cache = new LinkedBlockingDeque<Msg<?>>(maxCache);
            _cache.drainTo(cache, maxCache);
            _cache = cache;
        }

        sendCache();
    }

    void sendCache() {
        if (_cache == null || _cache.size() == 0 || getPlugin() == null)
            return;

        final Plugin p = getPlugin();
        final BlockingQueue<Msg<?>> cache = _cache;
        new Thread("SendCacheMsg") {
            public void run() {
                Method im = null; // interest method
                Method rm = null; // receive method
                for (Method m : getPlugin().getClass().getDeclaredMethods()) {
                    if (m.getAnnotation(MsgInterest.class) != null) {
                        im = m;
                        m.setAccessible(true);
                    } else if (m.getAnnotation(MsgRecv.class) != null) {
                        rm = m;
                        m.setAccessible(true);
                    }
                    if (im != null && rm != null) {
                        break;
                    }
                }

                Msg<?> msg = null;
                while ((msg = cache.poll()) != null) {
                    if (interestMsg(msg)) { // message type
                        if (im != null) {
                            try {
                                if (!((Boolean) im.invoke(p, msg)).booleanValue())
                                    continue;
                            } catch (Exception e) {
                                LOG.warn(e.getMessage());
                            }
                        }
                        if (rm != null) {
                            try {
                                rm.invoke(p, msg);
                                LOG.info("PluginRef send cache msg: {}", msg.toString());
                            } catch (Exception e) {
                                LOG.warn(e.getMessage());
                            }
                        }

                        try {
                            Thread.sleep(2);
                        } catch (InterruptedException e) {
                        }
                    }
                }
            }
        }.start();
    }

    public Plugin getPlugin() {
        return _ref.getPlugin();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
     * java.lang.reflect.Method, java.lang.Object[])
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // cache msg when plugin is updating
        if (_ref.isUpdating() && method.getName().equals(M_InterestMsg) && args.length > 0 && args[0] instanceof Msg) {
            // TODO pitfall store nasty msg when not use msgTypes
            if (interestMsg((Msg<?>) args[0])) {
                LOG.info("PluginRef recv cache msg: {}", args[0].toString());
                _cache.offer((Msg<?>) args[0]);// TODO queue is full
            }
            return false;
        }

        Plugin p = _ref.getPlugin();
        if (p == null) {
            // when plug-in is updating, p is null TODO
            return null;
        }

        if (method.equals(DispatchTarget.class.getDeclaredMethod(M_InterestMsg, new Class[] { Msg.class }))) {
            if (args.length > 0 && args[0] instanceof Msg) {
                // special message,default return false
                try {
                    if ((Msg<?>) args[0] instanceof PoisonMsg) {
                        return _ref.getPlugin().getClass().getAnnotation(Message.class).recvPoison();
                    } else if ((Msg<?>) args[0] instanceof ConfigMsg) {
                        return _ref.getPlugin().getClass().getAnnotation(Message.class).recvConfig();
                    }
                } catch (Exception e) {
                    return false;
                }
                // custom message
                if (!interestMsg((Msg<?>) args[0]))
                    return false;
                Method cm = getCacheMethod(M_InterestMsg);
                if (cm != null)
                    return cm.invoke(p, args);

                Class<?> clazz = p.getClass();
                do {
                    for (Method m : clazz.getDeclaredMethods()) {
                        if (m.getAnnotation(MsgInterest.class) != null) {
                            m.setAccessible(true);
                            putCacheMethod(M_InterestMsg, m);
                            return m.invoke(p, args);
                        }
                    }
                } while ((clazz = clazz.getSuperclass()) != null);
            }
            return false;
        } else if (method.equals(DispatchTarget.class.getDeclaredMethod(M_Receive, new Class[] { Msg.class }))) {
            Method cm = getCacheMethod(M_Receive);
            if (cm != null)
                return cm.invoke(p, args);

            Class<?> clazz = p.getClass();
            do {
                for (Method m : clazz.getDeclaredMethods()) {
                    if (m.getAnnotation(MsgRecv.class) != null) {
                        m.setAccessible(true);
                        putCacheMethod(M_Receive, m);
                        return m.invoke(p, args);
                    }
                }
            } while ((clazz = clazz.getSuperclass()) != null);
        }

        return method.invoke(p, args);
    }

    /**
     * @param msg
     * @return
     */
    boolean interestMsg(Msg<?> msg) {
        if (_types == null || _types.length == 0)
            return true;
        return Arrays.binarySearch(_types, msg.getType()) < 0 ? false : true;
    }

    private Method getCacheMethod(String name) {
        WeakReference<Method> wr = _mc.get(name);
        return null == wr ? null : wr.get();
    }

    private void putCacheMethod(String name, Method m) {
        _mc.put(name, new WeakReference<Method>(m));
    }

}
